"""
Contains the imap4 client, and helper functions
Written by: Daniel Ortiz
Date: December 8, 2007
Version: 0.0
"""
from twisted.mail import imap4
from twisted.internet import protocol, defer, threads
import email
from database.mapclass import create_mail

class IMAP4Client(imap4.IMAP4Client):
    def serverGreeting(self, greeting):
        """Logs the user into the IMAP4 server and then chains to the
        initialization code"""
        if not self.factory.check:
            login = self.login(self.factory.username, self.factory.password)
            login.addCallback(self.loggedIn)
        else:
            self.factory.f(True)
            self.logout().addErrback(self.IgnoreError)

    def IgnoreError(self, error):
        """Ignore error from the deferred"""
        pass

    def PrintError(self, error):
        """Function will print an error. Used for debuggin"""
        print error

    def loggedIn(self, results):
        """Login grabs a list of content from the server"""
        return self.list("", "*").addCallback(self.HandleMailboxList)

    def HandleMailboxList(self, ilist):
        """Parses the mailbox names into a list"""
        self.mailbox_list = [boxInfo[2] for boxInfo in ilist if not '\\Noselect' in boxInfo[0]]
        self.factory.handler.SetSynchGauge(self.mailbox_list, new = True)
        return self.Synchronize(self.mailbox_list)

    def GetMailbox(self, mailbox, mlist = None):
        """Get the relevant mailbox"""
        self.mlist = mlist
        self.folder = mailbox
        print self.mlist
        return self.select(mailbox).addErrback(self.PrintReason).addCallback(self.HandleSelect)

    def Search(self, **kw):
        """Search the entire mailbox for a string"""
        q = kw['query']
        flist = kw['flist']
        callback = kw['callback']
        m = kw['mailbox']
        self.search_result = []
        self.IterateSearch(flist, callback, q, m)
    
    def IterateSearch(self, flist, callback, q, m):
        """Iterate through the folders searching for the query"""
        if flist:
            f = flist.pop()
            d = self.select(f).addErrback(self.PrintError)
            d.addCallback(self.HandleSearch, f = flist, c = callback, q = q, m = m)
            return d
        else:
            callback(self.search_result, m)
            return defer.succeed(True)
            
    def HandleSearch(self, d, **kw):
        """Handle the search selection of a folder"""
        self.search(kw['q'], uid = True).addCallback(self.HandleSearchResults, **kw)
        
    def HandleSearchResults(self, uids, **kw):
        self.search_result = self.search_result + uids
        self.IterateSearch(kw['f'], kw['c'], kw['q'], kw['m'])
    
    def AddToSearch(self, buf):
        print 'Search:', buf
        return defer.succeed(True)

    def PrintReason(self, err):
        """Print an error to the command line. For debugging"""
        print err
        
    def HandleSelect(self, data):
        messages = imap4.MessageSet(1, None)
        print data
        return self.fetchUID(messages).addErrback(self.PrintReason).addCallback(self.HandleUID).addErrback(self.PrintReason)

    @defer.deferredGenerator
    def MoveFolder(self, flist, pfolder):
        for folder, uids in flist:
            d =  self.create(pfolder + "/" + folder)
            d.addCallback(lambda _: self.select(folder))
            d.addCallback(lambda _: self.copy(uids, pfolder + "/" + folder, uid = True))
            d.addCallback(lambda _: self.delete(folder))
            wfd = defer.waitForDeferred(d)
            yield wfd
            
    def NewFolder(self, folder):
        return self.create(folder)
    
    def DeleteFolder(self, folder):
        if folder in self.mailbox_list:
            self.delete(folder)
    @defer.deferredGenerator
    def Synchronize(self, mlist):
        """Synchronize the server with the local database"""
        def __handleUID(data, m):
            print data['RECENT'], self.factory.sync, m
            length = self.factory.handler.GetNumberOfEmail(self.factory.m, m)
            if data['RECENT'] or not self.factory.sync or data['EXISTS'] > length:
                message = imap4.MessageSet(1, None)
                return self.fetchUID(message)
            else:
                self.factory.handler.DisplayMailInGUI(self.factory.m, m)
                return defer.succeed({})
        def __handleAll(data, m):
            uids = [data[key]['UID'] for key in data.keys()]
            d =  self.factory.handler.PurgeUID(self.factory.m, uids, m)
            d.addCallback(__handleMessageSet)
            return d
        
        def __handleMessageSet(message_set):
            if message_set == imap4.MessageSet(1, None) or message_set:
                return self.fetchAll(message_set, uid = True)
            else:
                return defer.succeed({})
            
        for m in mlist:
            d = self.select(m)
            wfd = defer.waitForDeferred(d)
            yield wfd
            data = wfd.getResult()
            d = __handleUID(data, m)
            wfd = defer.waitForDeferred(d)
            yield wfd
            data = wfd.getResult()
            if data:
                d = __handleAll(data, m)
                wfd = defer.waitForDeferred(d)
                yield wfd
                data = wfd.getResult()
                mail = [create_mail(data[key]['FLAGS'], data[key]['UID'],data[key]['ENVELOPE'], m, self.factory.m.id) for key in data.keys()]
                self.factory.handler.SaveMail(mail, self.factory.m)
            self.factory.handler.IncGauge()
        self.factory.sync = False

#    def Synchronize(self, mlist):
#        """Synchronize the database with the server"""
#        #print 'Synch:', mlist
#        if mlist:
#            m = mlist.pop()
#            self.factory.handler.IncGauge()
#            self.select(m).addErrback(self.PrintError).addCallback(self.HandleSynchonize, c = self.Synchronize, l = mlist, folder = m)
#        else:
#            self.factory.sync = False
            
    def HandleSynchonize(self, data, **kw):
        """Check for data and synchronize appropriately"""
        if not data:
            return
        #print self.factory.sync
        if data['RECENT'] or not self.factory.sync:
            messages = imap4.MessageSet(1, None)
            d = self.fetchUID(messages)
            d.addErrback(self.PrintReason)
            d.addCallback(self.HandleUID, **kw)
            d.addCallback(lambda _: kw['c'](kw['l']))
        else:
            if self.factory.sync and not data['EXISTS']:
                self.factory.handler.DisplayMailInGUI(self.factory.m, kw['folder'] )
            kw['c'](kw['l'])

    def HandleUID(self, data, **kw):
        """Takes the UIDS from the server, places them in a list, and purges the uids, which currently
            reside in the client"""
        def __CreateUids():
            if data:
                return [data[key]['UID'] for key in data.keys()]
            else: 
                return []
        def __InnerPurge(uids):
            return self.factory.handler.PurgeUID(self.factory.m, uids, kw['folder'])
        def __FetchAll(messages):
            if messages == imap4.MessageSet(1, None) or messages:
                return self.fetchAll(messages, uid = True).addErrback(self.PrintReason).addCallback(self.HandleAll, **kw)
            else:
                return defer.succeed(True)
        d = threads.deferToThread(__CreateUids)
        d.addCallback(lambda _: threads.deferToThread(__InnerPurge, d.result))
        d.addCallback(__FetchAll)
        return d  

    def HandleAll(self, data, **kw):
        def __CreateMail():
            return [create_mail(data[key]['FLAGS'], data[key]['UID'],data[key]['ENVELOPE'], kw['folder'], self.factory.m.id) for key in data.keys()]
        def __SaveMail(mail):
            self.factory.handler.SaveMail(mail, self.factory.m)
            return True
        d = threads.deferToThread(__CreateMail)
        d.addCallback(lambda _: threads.deferToThread(__SaveMail, d.result))
        return d

    def GetMailboxSize(self, sel):
        """Gets the mailbox size, and decodes it for different transfer mechanisms"""
        self.sel = sel
        messages = imap4.MessageSet(1, None)
        return self.fetchSize(messages, True).addCallback(self.HandleSize).addErrback(self.PrintReason)

   


class IMAP4Factory(protocol.ClientFactory):
    protocol = IMAP4Client

    def __init__(self, username, password, f, fr, m,
                 check = False, h = None, s = None):
        """f - Callback for simple loopback interface to iterate over all mailboxes
           fr - Handles the results
        """
        self.username = username
        self.password = password
        self.buf = {}
        self.f = f
        self.fr = fr
        self.m = m
        self.check = check
        self.handler = h
        self.sync = s
        
    def buildProtocol(self, addr):
        """Save a reference to the protocol"""
        self.proto = self.protocol()
        self.proto.factory = self
        return self.proto

    def clientConnectionFailed(self, connector, err):
        """Ignore connection errors for now"""
        if self.check:
            self.f(None, err)

    def clientConnectionLost(self, connector, err):
        """Ignore lost connections for now"""
        pass

if __name__ == '__main__':
    def HandleError(reason):
        print reason
        reactor.stop()

    def HandleMailboxList(mlist, client):
        if mlist:
            m = mlist.pop(0)
            client.GetMailbox(m, mlist).addErrback(HandleError)
        else:
            client.logout().addErrback(HandleError)
            reactor.stop()

    def HandleResults(st, mlist, client, domain):
        print st
        HandleMailboxList(mlist, client)
        
    from twisted.internet import reactor, ssl
    from structures import imap4_google
    import getpass
    username = raw_input('Username: ')
    password = getpass.getpass("Password: ")
    f = IMAP4Factory(username, password, HandleMailboxList, HandleResults)
    reactor.connectSSL(imap4_google['iserver'], imap4_google['iport'], f,
                       ssl.ClientContextFactory())
    reactor.run()
    
